import random
from pathlib import Path
from typing import List, Optional

from PyQt5.QtCore import QEasingCurve, QPointF, QPropertyAnimation, QParallelAnimationGroup, Qt, QTimer, QRectF, QFileSystemWatcher
from PyQt5.QtGui import QBrush, QColor, QPainter, QPixmap
from PyQt5.QtWidgets import QGraphicsObject, QGraphicsScene, QGraphicsView, QSizePolicy, QWidget, QVBoxLayout

from .slideshow import list_image_files


class PhotoCardItem(QGraphicsObject):
    def __init__(self, pixmap: QPixmap, border_px: int = 16, shadow: bool = True):
        super().__init__()
        self._pixmap = pixmap
        self._border_px = border_px
        self._shadow = shadow
        # Card size is pixmap size + borders
        self._inner_rect = QRectF(0, 0, self._pixmap.width(), self._pixmap.height())
        self._outer_rect = QRectF(
            -self._border_px,
            -self._border_px,
            self._pixmap.width() + self._border_px * 2,
            self._pixmap.height() + self._border_px * 2,
        )

    def boundingRect(self) -> QRectF:  # noqa: N802
        # Add a little margin for shadow
        if self._shadow:
            return self._outer_rect.adjusted(-8, -8, 8, 8)
        return self._outer_rect

    def paint(self, painter: QPainter, option, widget=None):  # noqa: N802
        painter.setRenderHint(QPainter.Antialiasing, True)
        # Shadow
        if self._shadow:
            shadow_color = QColor(0, 0, 0, 90)
            painter.setBrush(QBrush(shadow_color))
            painter.setPen(Qt.NoPen)
            painter.drawRoundedRect(self._outer_rect.translated(4, 6), 12, 12)

        # White card background (border)
        painter.setBrush(QBrush(QColor(255, 255, 255)))
        painter.setPen(QColor(230, 230, 230))
        painter.drawRoundedRect(self._outer_rect, 12, 12)

        # Photo area
        painter.drawPixmap(self._inner_rect.topLeft(), self._pixmap)


class FancySlideshow(QWidget):
    def __init__(
        self,
        image_dir: Path,
        interval_ms: int = 5000,
        angle_range_deg: int = 15,
        min_height_ratio: float = 0.6,
        max_height_ratio: float = 1.0,
        stack_size: int = 4,
        in_duration_ms: int = 1200,
        out_duration_ms: int = 800,
        shuffle: bool = True,
    ) -> None:
        super().__init__()
        self.image_dir = image_dir
        self.interval_ms = max(1000, interval_ms)
        self.angle_range_deg = max(0, int(angle_range_deg))
        self.min_height_ratio = max(0.1, float(min_height_ratio))
        self.max_height_ratio = min(1.0, max(self.min_height_ratio, float(max_height_ratio)))
        self.stack_size = max(1, int(stack_size))
        self.in_duration_ms = max(100, int(in_duration_ms))
        self.out_duration_ms = max(100, int(out_duration_ms))
        self.shuffle = shuffle

        self._images: List[Path] = list_image_files(self.image_dir)
        try:
            print(f"FancySlideshow: loaded {len(self._images)} images from {self.image_dir}")
        except Exception:
            pass
        if self.shuffle:
            random.shuffle(self._images)
        self._index: int = -1

        # Graphics scene/view setup
        self._scene = QGraphicsScene(self)
        self._view = QGraphicsView(self._scene, self)
        self._view.setFrameShape(QGraphicsView.NoFrame)
        self._view.setStyleSheet("background: #000;")
        self._view.setAlignment(Qt.AlignCenter)
        self._view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._view.setRenderHints(
            self._view.renderHints()
            | QPainter.Antialiasing
            | QPainter.SmoothPixmapTransform
        )

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._view)

        self._timer = QTimer(self)
        self._timer.setInterval(self.interval_ms)
        self._timer.timeout.connect(self._advance)

        # 设置文件夹监控
        self._file_watcher = QFileSystemWatcher(self)
        self._file_watcher.addPath(str(self.image_dir))
        self._file_watcher.directoryChanged.connect(self._on_directory_changed)

        # Hold animations to prevent GC
        self._live_anims: List[object] = []
        self._has_rendered_first: bool = False
        # Maintain FIFO queue of cards to ensure earliest-in fades first
        self._card_queue: List[PhotoCardItem] = []

        # First render immediately
        self._view.setSceneRect(QRectF(0, 0, float(self.width()), float(self.height())))
        QTimer.singleShot(0, lambda: self._view.setSceneRect(QRectF(0, 0, float(self.width()), float(self.height()))))
        if not self._images:
            # 在场景中央显示占位提示
            from PyQt5.QtWidgets import QGraphicsTextItem
            text = QGraphicsTextItem(f"未找到图片\n{self.image_dir}")
            text.setDefaultTextColor(QColor(220, 220, 220))
            text.setPos(40, 40)
            self._scene.addItem(text)
        else:
            self._advance()
        self._timer.start()

    def resizeEvent(self, event):  # noqa: N802
        super().resizeEvent(event)
        self._view.setSceneRect(QRectF(0, 0, float(self.width()), float(self.height())))

    def showEvent(self, event):  # noqa: N802
        super().showEvent(event)
        # 确保在显示后根据真实尺寸设置场景矩形，并触发一次渲染
        self._view.setSceneRect(QRectF(0, 0, float(self.width()), float(self.height())))
        if not self._has_rendered_first and self._images:
            QTimer.singleShot(0, self._advance)

    def _pick_next_image(self) -> Optional[Path]:
        if not self._images:
            return None
        self._index = (self._index + 1) % len(self._images)
        if self.shuffle and self._index == 0:
            random.shuffle(self._images)
        return self._images[self._index]

    def _prepare_pixmap(self, img_path: Path) -> Optional[QPixmap]:
        pm = QPixmap(str(img_path))
        if pm.isNull():
            return None
        # Scale to fit height ratio
        h = self.height() if self.height() > 0 else self.sizeHint().height() or 1080
        target_h = int(h * random.uniform(self.min_height_ratio, self.max_height_ratio))
        if target_h <= 0:
            target_h = pm.height()
        scaled = pm.scaledToHeight(target_h, Qt.SmoothTransformation)
        return scaled

    def _advance(self) -> None:
        img = self._pick_next_image()
        if img is None:
            return
        pm = self._prepare_pixmap(img)
        if pm is None:
            return

        card = PhotoCardItem(pm, border_px=16, shadow=True)

        # Random rotation within angle range
        angle = random.uniform(-self.angle_range_deg, self.angle_range_deg)
        card.setRotation(angle)

        # Target position: spread across the screen, not always dead center
        scene_rect = self._view.sceneRect()
        card_w = card.boundingRect().width()
        card_h = card.boundingRect().height()

        # Safe margins to keep full card inside screen
        margin = 24.0
        min_x = float(scene_rect.left()) + margin
        max_x = float(scene_rect.right()) - margin - card_w
        min_y = float(scene_rect.top()) + margin
        max_y = float(scene_rect.bottom()) - margin - card_h
        if max_x < min_x:
            min_x = float(scene_rect.left())
            max_x = min_x
        if max_y < min_y:
            min_y = float(scene_rect.top())
            max_y = min_y

        # Bias away from exact center to avoid heavy overlap
        cx = scene_rect.width() / 2.0
        cy = scene_rect.height() / 2.0
        min_radius = 0.28 * min(scene_rect.width(), scene_rect.height())
        # Try a few samples to satisfy distance-from-center
        for _ in range(8):
            candidate_x = random.uniform(min_x, max_x)
            candidate_y = random.uniform(min_y, max_y)
            center_x = candidate_x + card_w / 2.0
            center_y = candidate_y + card_h / 2.0
            dx = center_x - cx
            dy = center_y - cy
            if (dx * dx + dy * dy) ** 0.5 >= min_radius:
                target_pos = QPointF(candidate_x, candidate_y)
                break
        else:
            # Fallback: allow near-center placement with a stronger random offset
            candidate_x = random.uniform(min_x, max_x)
            candidate_y = random.uniform(min_y, max_y)
            target_pos = QPointF(candidate_x, candidate_y)

        # Start position: above the top, with a bit more negative y (falling effect)
        start_pos = QPointF(target_pos.x(), -card.boundingRect().height() - 80)
        card.setPos(start_pos)
        card.setOpacity(0.0)

        self._scene.addItem(card)
        self._has_rendered_first = True

        # Queue management: newest at tail
        self._card_queue.append(card)
        # Assign z-order based on queue order (oldest lowest z)
        for idx, it in enumerate(self._card_queue, start=1):
            it.setZValue(float(idx))

        # Enforce stack size: remove oldest beyond capacity (strict FIFO)
        while len(self._card_queue) > self.stack_size:
            oldest = self._card_queue.pop(0)
            self._fade_and_remove(oldest)

        # In animations: position fall + slight rotation settle + fade in
        anim_pos = QPropertyAnimation(card, b"pos", self)
        anim_pos.setDuration(self.in_duration_ms)
        anim_pos.setStartValue(start_pos)
        anim_pos.setEndValue(target_pos)
        anim_pos.setEasingCurve(QEasingCurve.OutBounce)

        anim_opacity_in = QPropertyAnimation(card, b"opacity", self)
        anim_opacity_in.setDuration(int(self.in_duration_ms * 0.6))
        anim_opacity_in.setStartValue(0.0)
        anim_opacity_in.setEndValue(1.0)
        anim_opacity_in.setEasingCurve(QEasingCurve.OutCubic)

        group_in = QParallelAnimationGroup(self)
        group_in.addAnimation(anim_pos)
        group_in.addAnimation(anim_opacity_in)

        def on_group_finished():
            try:
                self._live_anims.remove(group_in)
            except ValueError:
                pass

        group_in.finished.connect(on_group_finished)
        self._live_anims.append(group_in)
        group_in.start()

        # Manage stack: fade out older items beyond stack_size
        items = [it for it in self._scene.items() if isinstance(it, PhotoCardItem)]
        # items() returns in z-order; ensure newest is on top
        card.setZValue(len(items) + 1)
        if len(items) > self.stack_size:
            # Fade out the oldest ones (those with lowest z)
            # Collect PhotoCardItems sorted by z ascending
            photo_items: List[PhotoCardItem] = sorted(
                [it for it in items if isinstance(it, PhotoCardItem)], key=lambda i: i.zValue()
            )
            to_remove = len(items) - self.stack_size
            for old in photo_items[:to_remove]:
                self._fade_and_remove(old)

    def _fade_and_remove(self, item: PhotoCardItem) -> None:
        anim = QPropertyAnimation(item, b"opacity", self)
        anim.setDuration(self.out_duration_ms)
        anim.setStartValue(item.opacity())
        anim.setEndValue(0.0)
        anim.setEasingCurve(QEasingCurve.InOutCubic)

        def on_finished():
            self._scene.removeItem(item)
            item.deleteLater()
            try:
                self._live_anims.remove(anim)
            except ValueError:
                pass

        anim.finished.connect(on_finished)
        self._live_anims.append(anim)
        anim.start()

    def _on_directory_changed(self, path: str) -> None:
        """当文件夹发生变化时重新扫描图片列表"""
        print(f"FancySlideshow: 检测到文件夹变化: {path}")
        old_count = len(self._images)
        self._images = list_image_files(self.image_dir)
        new_count = len(self._images)
        
        if new_count > old_count:
            print(f"FancySlideshow: 发现 {new_count - old_count} 张新图片，总共 {new_count} 张")
            # 如果启用了随机播放，重新打乱列表
            if self.shuffle:
                random.shuffle(self._images)
        elif new_count < old_count:
            print(f"FancySlideshow: 删除了 {old_count - new_count} 张图片，剩余 {new_count} 张")
        
        # 如果当前索引超出范围，重置索引
        if self._images and self._index >= len(self._images):
            self._index = 0


